const path = require('path')
const fs = require('fs');

const types = require('babel-types');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const baseDir = toUnitPath(process.cwd());// 上下文目录
function toUnitPath(filePath) {
  return filePath.replace(/\\/g, '/');
}

class Complication {
  constructor(options) {
    this.options = options;

    // webpack4使用数组，webpack5使用set（去重）
    this.modules = []; // 存放所有的模块
    this.entries = [];// 存放所有入口
    this.chunks = [];// 存放所有代码
    this.assets = {};// 所有产出资源
    this.files = [];// 所有产出文件
  }
  build(callback) {
    // 5.根据配置中的entry找入口文件
    let entry = {}
    if (typeof this.options.entry === 'string') {
      entry.main = this.options.entry;
    } else {
      entry = this.options.entry;
    }

    // entry:{entry1:'./src/entry1.js',entry2:'./src/entry2.js'}
    for (let entryName in entry) {
      let entryFilePath = toUnitPath(path.join(this.options.context, entry[entryName]));
      console.log(entryFilePath);
      // let entryFilePath = path.join(this.options.context, entry[entryName]);
      // 6.从入口文件出发，调用所有配置的loader对模块进行编译，一个入口文件进行一次编译
      let entryModule = this.buildModule(entryName, entryFilePath);
       // { id: title.js, name:index.js, dependencies: [], extraNames: [] }
     // { id: moduleId, name:, dependencies: [], extraNames: [index.js] } entry
      /* this.modules.push(entryModule) 注释掉因为webpack不在modules存入口文件信息*/
      //8.根据入口模块和依赖的关系，组装成一个个包含多个模块的chunk
      let chunk = {
        name: entryName, entryModule, modules: this.modules.filter(item => {
          return item.name == entryName || item.extraNames.includes(entryName)
        })
      }
      this.chunks.push(chunk);
      this.entries.push(chunk)
    }
    // 9.再把每个chunk转换成一个单独的文件加入到输出列表
    this.chunks.forEach(chunk => {
      let filename = this.options.output.filename;
      this.assets[filename] = getSource(chunk)
    })

    callback(null, {
      entries: this.entries,
      chunks: this.chunks,
      modules: this.modules,
      files: this.files,
      assets: this.assets
    })
  }
  // name=模块名称，modulePath=模块绝对路径
  buildModule(name, modulePath) {
    // 6.从入口文件触发，调用所有配置的loader对模块进行编译
    console.log(modulePath, 'modulePath')
    debugger
    let sourceCode = fs.readFileSync(modulePath, 'utf-8');
    let rules = this.options.module.rules;
    let loaders = []; // 寻找匹配的loader
    for (let i = 0; i < rules.length; i++) {
      let { test } = rules[i];
      if (test.test(modulePath)) {
        loaders = [...loaders, ...rules[i].use]
      }
    }
    // loader从右往左，执行，将上一次编译后的结果传个下个插件
    sourceCode = loaders.reduceRight((sourceCode, loader) => {
      return require(loader)(sourceCode)
    }, sourceCode)

    //7.在找出该模块依赖的模块，在递归本步骤直到所有入口依赖的文件都经过了本步骤处理
    // 获取当前模块id (模块id的值：当前文件相对于当前根目录的路径) ./src/index.js
    // let moduleId = 
    // 进行处理
    let ast = parser.parse(sourceCode, { sourceType: 'module' });
    // 当前模块模块id ./src/index.js
    let moduleId = './' + path.posix.relative(baseDir, modulePath);
    // path.relative(from, to), 根据当前工作目录返回from到to的相对路径，
    //在posix上返回合适的分割符 path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
    // 返回: '../../impl/bbb'
    // 如果不使用posix，在windows上返回  '..\\..\\impl\\bbb'
    let module = { id: moduleId, name, dependencies: [], extraNames: [] }
    traverse(ast, {
      CallExpression: ({ node }) => {// 函数调用节点 ，捕获特别类型的节点 ，
        if (node.callee.name === 'require') {// 获取require()类型的节点
          let moduleName = node.arguments[0].value; // ./title.js
          // 获取当前文件所在目录，并通过path.posix方法将路径分割符固定选为 /  win下默认为\
          let dirname = path.posix.dirname(modulePath)//默认win下为： D:\b-jiagou\webpack\04flow>\src ，现在为：D:/b-jiagou/webpack/04flow/src
          console.log('dirname', dirname)
          let depModulePath = path.posix.join(dirname, moduleName);
          // console.log(depModulePath);
          let extensions = this.options.resolve.extensions;
          depModulePath = tryExtensions(depModulePath, extensions);// 处理入口文件中require未写文件类型的情况
          // 得到依赖文件对应的模块id  依赖文件：D:/b-jiagou/webpack/04flow/src/title.js
          // 模块id：./src/title.js  即相对于根目录的相对路径
          let depModuleId = './' + path.posix.relative(baseDir, depModulePath)// 获取depModulePath相对于根目录的相对路径
          node.arguments = [types.stringLiteral(depModuleId)];
          module.dependencies.push({ depModuleId, depModulePath })
        }
      }
    })
    let { code } = generator(ast);
    module._source = code// 模块源代码指向语法树转换后新生成的源代码
    module.dependencies.forEach(({ depModuleId, depModulePath }) => {

     // { id: title.js, name:index.js, dependencies: [], extraNames: [] }
     // { id: moduleId, name:, dependencies: [], extraNames: [index.js] } entry
      let depModule = this.modules.find(item => item.id === depModuleId);
      if (depModule) {// 该模块已被其他模块引入了，不在浪费时间编译了，添加到extraNames中
        depModule.extraNames.push(name)

      } else {
        let dependencyModule = this.buildModule(name, depModulePath);
        this.modules.push(dependencyModule)
      }
    })
    console.log('loader处理代码', sourceCode)
    return module;
  }
}
function getSource(chunk) {
  return `
  (() => {
      var modules = ({
          ${chunk.modules.map(module => `
                  "${module.id}":(module,exports,require)=>{
                      ${module._source}
                  }
              `).join(',')
    }
      });
      var cache = {};
      function require(moduleId) {
        var cachedModule = cache[moduleId];
        if (cachedModule !== undefined) {
          return cachedModule.exports;
        }
        var module = cache[moduleId] = {
          exports: {}
        };
        modules[moduleId](module, module.exports, require);
        return module.exports;
      }
      var exports = {};
      (() => {
       ${chunk.entryModule._source}
      })();
    })()
      ;
  `
}
function tryExtensions(modulePath, extensions) {
  // require的文件带有后缀名的情况，给extensions添加一个空
  extensions.unshift('');
  for (let i = 0; i < extensions.length; i++) {
    let filePath = modulePath + extensions[i];// c:/xx/xx/xx/index.js
    console.log(filePath, '------')
    if (fs.existsSync(filePath)) { // 判断处理后缀的情况后，如果文件存在则返回该路径
      return filePath
    }
  }
  throw new Error('Module not found')
}
module.exports = Complication